Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

3장. 조건과 반복

프로그램은 늘 같은 일만 하지 않습니다.

상황에 따라 다르게 행동하고,
같은 일을 여러 번 반복하기도 합니다.

“로그인한 사용자면 이 화면을,
아니면 저 화면을 보여 준다.”
이런 판단이 조건입니다.

“목록에 있는 상품을 하나씩 꺼내
가격을 더한다.”
이런 되풀이가 반복입니다.

이 장에서는 코틀린에서 조건과 반복을
어떻게 다루는지 배웁니다.

특히 코틀린의 조건문은
자바와 결정적으로 다른 점이 하나 있습니다.
바로 “값을 돌려준다“는 점입니다.

이 작은 차이가 코드를 얼마나 깔끔하게 만드는지
직접 확인해 봅시다.


3.1 표현식으로서의 if

문장과 표현식

먼저 두 단어를 구분하고 시작하겠습니다.
문장과 표현식입니다.

  • 문장(statement) : 일을 시키기만 하는 코드
  • 표현식(expression) : 값으로 계산되는 코드

예를 들어 3 + 4는 표현식입니다.
7이라는 값으로 바뀌기 때문입니다.

반면 “화면에 출력하라” 같은 명령은
값을 남기지 않으므로 문장에 가깝습니다.

이 구분이 왜 중요할까요?
코틀린의 if가 바로 표현식이기 때문입니다.

값을 반환하는 if

자바에서 if는 문장이었습니다.
판단만 하고 값을 남기지 않았습니다.

하지만 코틀린에서 if는 값을 돌려줍니다.
즉, if의 결과를 변수에 바로 담을 수 있습니다.

먼저 익숙한 형태부터 보겠습니다.

val score = 85

if (score >= 60) {
    println("합격")
} else {
    println("불합격")
}

여기까지는 자바와 거의 같습니다.
조건이 참이면 위쪽을, 거짓이면 아래쪽을 실행합니다.

이제 코틀린다운 방식을 보겠습니다.

val score = 85

val result = if (score >= 60) "합격" else "불합격"

println(result)   // 합격

if 전체가 하나의 값이 되어
result에 담겼습니다.

조건이 참이면 "합격"이,
거짓이면 "불합격"result의 값이 됩니다.

중괄호를 쓰는 형태에서도 값을 돌려받을 수 있습니다.
이때는 블록의 마지막 줄이 그 값이 됩니다.

val score = 85

val grade = if (score >= 90) {
    "A"
} else if (score >= 60) {
    "B"
} else {
    "F"
}

println(grade)   // B

각 블록의 마지막 줄에 적힌 값이
곧 그 블록의 결과입니다.

블록에서는
마지막 줄이 돌려주는 값이 된다.

이 규칙은 앞으로 나올 when에서도
그대로 쓰이니 기억해 둡시다.

삼항 연산자가 없는 이유

자바에는 삼항 연산자라는 것이 있었습니다.
조건 ? 참일_때 : 거짓일_때 형태입니다.

// 자바
String result = (score >= 60) ? "합격" : "불합격";

짧게 쓸 수 있어 편했지만,
기호가 많아 처음 보면 읽기 어려웠습니다.

그런데 코틀린에는 삼항 연산자가 없습니다.
일부러 넣지 않았습니다.

이미 if가 값을 돌려주기 때문입니다.

val result = if (score >= 60) "합격" else "불합격"

자바의 삼항 연산자와 하는 일이 똑같습니다.
그러면서도 ifelse라는
읽기 쉬운 단어를 그대로 씁니다.

정리하면 이렇습니다.

자바코틀린
if 문 (값 없음)if 표현식 (값 있음)
삼항 연산자 ? :if 표현식이 대신함

코틀린은 문법을 하나 없애는 대신,
기존 if를 더 강하게 만들었다.

한 가지 주의할 점이 있습니다.
if를 값으로 쓸 때는 else가 반드시 있어야 합니다.

조건이 거짓일 때 돌려줄 값이 없으면
변수에 무엇을 담을지 정할 수 없기 때문입니다.

// 오류: else가 없어 거짓일 때 값이 없음
val result = if (score >= 60) "합격"

else까지 갖추면
어떤 경우든 값이 정해지므로 안전합니다.


3.2 when

값에 따른 분기

조건이 두세 개면 if로 충분합니다.
하지만 경우의 수가 많아지면 어떨까요?

val grade = "B"

if (grade == "A") {
    println("훌륭해요")
} else if (grade == "B") {
    println("잘했어요")
} else if (grade == "C") {
    println("괜찮아요")
} else {
    println("분발해요")
}

else if가 계속 이어지면
읽기가 점점 힘들어집니다.

이럴 때 쓰는 것이 when입니다.
자바의 switch와 비슷한 자리입니다.

val grade = "B"

when (grade) {
    "A" -> println("훌륭해요")
    "B" -> println("잘했어요")
    "C" -> println("괜찮아요")
    else -> println("분발해요")
}

훨씬 깔끔해졌습니다.

읽는 법은 간단합니다.
grade의 값이 화살표(->) 왼쪽과 같으면
오른쪽을 실행합니다.

어디에도 해당하지 않으면
else가 실행됩니다.

자바의 switch를 아는 분이라면
반가운 점이 있을 것입니다.

  • break를 적지 않아도 된다
  • case라는 단어를 반복하지 않아도 된다

자바 switch에서는 break를 빠뜨리면
다음 항목까지 줄줄이 실행되는 실수가 잦았습니다.
코틀린 when은 그런 걱정이 없습니다.

여러 조건 묶기

여러 값을 같은 방식으로 처리하고 싶을 때가 있습니다.
이때는 값을 쉼표(,)로 나열합니다.

val day = "토"

when (day) {
    "토", "일" -> println("주말입니다")
    else -> println("평일입니다")
}
// 출력: 주말입니다

"토" 또는 "일"이면
같은 줄을 실행합니다.

자바에서 case를 여러 줄 늘어놓던 것을
한 줄로 묶은 셈입니다.

범위 검사

값이 어떤 범위 안에 드는지도 검사할 수 있습니다.
이때는 in과 범위 표현을 함께 씁니다.

val score = 85

when (score) {
    in 90..100 -> println("A")
    in 60..89 -> println("B")
    else -> println("F")
}
// 출력: B

in 90..100
“90부터 100까지 사이에 있는가“를 뜻합니다.

여기서 90..100 같은 표현을 범위라고 부릅니다.
범위는 다음 절(3.3)에서 자세히 다룹니다.

지금은 when 안에서
숫자 구간을 이렇게 나눌 수 있다는 점만 봐 둡시다.

타입 검사

값의 타입이 무엇인지에 따라
다르게 처리할 수도 있습니다.

이때는 is라는 키워드를 씁니다.
is는 “이 타입인가“를 묻습니다.

val value: Any = "안녕하세요"

when (value) {
    is Int -> println("정수입니다")
    is String -> println("길이는 ${value.length}")
    else -> println("모르는 타입입니다")
}
// 출력: 길이는 5

valueString이면
그 줄 안에서는 value를 문자열로 다룰 수 있습니다.

그래서 value.length처럼
문자열 전용 기능을 바로 쓸 수 있습니다.

여기서 Any 타입이 다시 나왔습니다.
2.4에서 배운 “무엇이든 담는 타입“입니다.

is로 검사하면 그 안에서
자동으로 타입이 좁혀집니다.
이 편리한 기능은 3부에서 더 깊이 다룹니다.

값을 반환하는 when

if가 값을 돌려주었듯이
when도 값을 돌려줍니다.

앞의 예제를 값으로 받아 보겠습니다.

val score = 85

val grade = when {
    score >= 90 -> "A"
    score >= 60 -> "B"
    else -> "F"
}

println(grade)   // B

여기서 눈여겨볼 점이 있습니다.
when 뒤의 괄호가 사라졌습니다.

괄호 없이 쓰면
화살표 왼쪽에 조건을 자유롭게 적을 수 있습니다.

when {
    조건1 -> 값1
    조건2 -> 값2
    else -> 기본값
}

이 형태는 사실상
if ... else if ...를 대신합니다.
그러면서 훨씬 읽기 좋습니다.

when을 값으로 쓸 때도 규칙이 하나 있습니다.
모든 경우를 빠짐없이 다뤄야 합니다.

값을 돌려줘야 하는데
빠진 경우가 있으면 값을 정할 수 없기 때문입니다.
그래서 보통 else를 함께 적습니다.

정리하면 이렇습니다.

if가 값을 돌려주듯
when도 값을 돌려준다.
여러 갈래를 값으로 정할 때 가장 편하다.


3.3 범위와 반복

Range

앞에서 잠깐 나온 90..100
이제 제대로 살펴보겠습니다.

..을 쓰면 범위(Range)를 만들 수 있습니다.
범위란 “어디부터 어디까지“를 나타내는 값입니다.

val numbers = 1..5   // 1, 2, 3, 4, 5

1..5는 1부터 5까지를 뜻합니다.
여기서 끝 숫자인 5도 포함된다는 점이 중요합니다.

1..5
1, 2, 3, 4, 5 다섯 개를 모두 포함한다.

in과 !in

어떤 값이 범위 안에 있는지 확인할 때
in을 씁니다.

val age = 20

println(age in 1..100)   // true
println(age in 1..10)    // false

age in 1..100
“age가 1부터 100 사이에 있는가“를 묻습니다.

반대로 범위 밖에 있는지 확인하려면
!in을 씁니다.
앞의 느낌표(!)가 “아니다“를 뜻합니다.

val age = 20

println(age !in 1..10)   // true (10보다 크므로 밖에 있음)

이 표현은 ifwhen과도 잘 어울립니다.

val score = 75

if (score in 60..100) {
    println("합격 범위입니다")
}

60 <= score && score <= 100
score in 60..100 한 번으로 쓴 것입니다.
훨씬 읽기 쉽습니다.

for

이제 반복을 배워 봅시다.
가장 많이 쓰는 것이 for입니다.

for는 여러 값을 하나씩 꺼내
차례로 처리합니다.

for (i in 1..5) {
    println(i)
}

출력은 이렇습니다.

1
2
3
4
5

읽는 법은 이렇습니다.
1..5 범위에서 값을 하나씩 꺼내 i에 담고,
그때마다 중괄호 안을 실행하라.”

자바를 아는 분이라면
차이가 확 느껴질 것입니다.

// 자바
for (int i = 1; i <= 5; i++) {
    System.out.println(i);
}

자바는 시작값, 조건, 증가를
세 부분으로 나눠 적어야 했습니다.

코틀린은 “이 범위를 돈다“고만 적으면 됩니다.
숫자를 직접 세거나 증가시킬 필요가 없습니다.

for는 범위뿐 아니라
목록 같은 값도 순회할 수 있습니다.

val fruits = listOf("사과", "바나나", "포도")

for (fruit in fruits) {
    println(fruit)
}

출력은 이렇습니다.

사과
바나나
포도

여기서 listOf는 목록을 만드는 방법입니다.
목록(리스트)은 6부에서 자세히 다룹니다.
지금은 “여러 값을 담은 묶음” 정도로 보면 됩니다.

while

정해진 범위를 도는 것이 아니라
“어떤 조건이 참인 동안” 반복하고 싶을 때가 있습니다.
이때는 while을 씁니다.

var count = 1

while (count <= 3) {
    println("count = $count")
    count++
}

출력은 이렇습니다.

count = 1
count = 2
count = 3

while은 괄호 안의 조건이 참인 동안
중괄호 안을 계속 반복합니다.

여기서 count++
count를 1 늘리라는 뜻입니다.

이 줄이 없으면 조건이 영원히 참이라
반복이 끝나지 않습니다.
이런 상태를 무한 루프(infinite loop)라고 부릅니다.

while을 쓸 때는
언젠가 조건이 거짓이 되도록
값을 꼭 바꿔 줘야 한다.

forwhile은 이렇게 구분하면 쉽습니다.

  • for : 몇 번 돌지 대강 정해져 있을 때
  • while : 언제 끝날지 조건에 달려 있을 때

until / downTo / step

범위를 만드는 방법을 조금 더 알아봅시다.
상황에 따라 편한 표현이 나뉩니다.

먼저 until입니다.
..과 달리 끝 숫자를 포함하지 않습니다.

for (i in 1 until 5) {
    print("$i ")
}
// 출력: 1 2 3 4

1 until 5
1부터 4까지입니다. (5는 빠짐)

목록을 다룰 때 이 표현이 특히 편합니다.
목록의 자리 번호는 0부터 시작하기 때문입니다.
(자리 번호는 6부에서 자세히 배웁니다.)

다음은 downTo입니다.
숫자를 거꾸로 세고 싶을 때 씁니다.

for (i in 5 downTo 1) {
    print("$i ")
}
// 출력: 5 4 3 2 1

5 downTo 1
5부터 1까지 하나씩 내려갑니다.

마지막은 step입니다.
건너뛰는 간격을 정할 때 씁니다.

for (i in 1..10 step 2) {
    print("$i ")
}
// 출력: 1 3 5 7 9

step 2는 두 칸씩 건너뛰라는 뜻입니다.
그래서 홀수만 출력됩니다.

downTostep을 함께 쓸 수도 있습니다.

for (i in 10 downTo 1 step 3) {
    print("$i ")
}
// 출력: 10 7 4 1

정리하면 이렇습니다.

표현
1..5끝 숫자 포함1,2,3,4,5
1 until 5끝 숫자 제외1,2,3,4
5 downTo 1거꾸로5,4,3,2,1
1..10 step 2간격 두고1,3,5,7,9

이 표현들만 알아도
대부분의 반복을 자연스럽게 쓸 수 있습니다.


3.4 구조 분해

Destructuring Declaration

값 하나에 여러 정보가 담겨 있을 때가 있습니다.
예를 들어 좌표는 x와 y를 함께 가집니다.

이런 값에서 여러 조각을
한 번에 꺼내는 방법이 있습니다.
구조 분해(Destructuring Declaration)라고 부릅니다.

말이 어렵지만 예를 보면 쉽습니다.

val (name, age) = Pair("홍길동", 20)

println(name)   // 홍길동
println(age)    // 20

Pair는 값 두 개를 하나로 묶는 도구입니다.
Pair("홍길동", 20)
이름과 나이를 한 덩어리로 만듭니다.

그 덩어리를 val (name, age)로 받으면
두 값이 각각의 변수로 풀립니다.

하나로 묶인 값을
여러 변수로 한 번에 풀어내는 것,
이것이 구조 분해다.

이렇게 안 하면 이렇게 써야 합니다.

val pair = Pair("홍길동", 20)
val name = pair.first    // 첫 번째 값
val age = pair.second    // 두 번째 값

구조 분해를 쓰면
이 과정을 한 줄로 줄일 수 있습니다.

Pair와 Map 순회

구조 분해는 반복문과 만나면 더 빛납니다.
특히 Map을 순회할 때 자주 씁니다.

Map은 “짝지어진 값“을 담는 묶음입니다.
이름표(키)와 내용물(값)이 짝을 이룹니다.
(Map은 6부에서 자세히 다룹니다.)

val ages = mapOf(
    "홍길동" to 20,
    "이순신" to 35
)

for ((name, age) in ages) {
    println("$name 님은 $age 세입니다")
}

출력은 이렇습니다.

홍길동 님은 20세입니다
이순신 님은 35세입니다

for ((name, age) in ages) 부분을 봅시다.
Map에서 짝을 하나씩 꺼내면서,
그 자리에서 바로 키와 값으로 풀었습니다.

여기서 "홍길동" to 20
to도 짚어 보겠습니다.

to는 두 값을 짝으로 묶어 줍니다.
사실 앞에서 본 Pair를 만드는 것과 같습니다.

val p1 = Pair("홍길동", 20)
val p2 = "홍길동" to 20      // 위와 완전히 같음

to를 쓰면 더 읽기 편해서
Map을 만들 때 주로 이 방식을 씁니다.

여러 값을 자연스럽게 다루기

구조 분해는 목록을 돌 때도 쓸 수 있습니다.
번호와 값을 함께 다루고 싶을 때 특히 편합니다.

val fruits = listOf("사과", "바나나", "포도")

for ((index, fruit) in fruits.withIndex()) {
    println("$index 번: $fruit")
}

출력은 이렇습니다.

0 번: 사과
1 번: 바나나
2 번: 포도

withIndex()는 각 값에
자리 번호를 붙여 줍니다.

그 번호와 값을
(index, fruit)로 한 번에 풀어 받았습니다.

자바에서는 번호를 세는 변수를
따로 만들어 관리해야 했습니다.
코틀린은 이 과정을 문법으로 깔끔하게 정리했습니다.

정리하면 구조 분해는
이런 상황에서 빛을 발합니다.

  • 값 두세 개가 하나로 묶여 있을 때
  • Map의 키와 값을 함께 다룰 때
  • 목록의 번호와 값을 함께 다룰 때

여러 값을 한 번에 풀어
이름을 붙이면 코드가 훨씬 읽기 쉬워진다.

구조 분해의 원리는 사실
“데이터 클래스“라는 개념과 이어집니다.
이 내용은 4부에서 다시 만나게 됩니다.


3장을 마치며

이 장에서 우리는 다음을 배웠습니다.

  • 코틀린의 if는 값을 돌려주는 표현식이고,
    그래서 삼항 연산자가 필요 없다는 점
  • 갈래가 많을 때는 when이 깔끔하고,
    값 비교뿐 아니라 범위와 타입 검사까지 된다는 점
  • .., until, downTo, step으로
    다양한 범위를 만들고 for로 순회한다는 점
  • while은 조건이 참인 동안 반복한다는 점
  • 구조 분해로 여러 값을
    한 번에 풀어 다룰 수 있다는 점

특히 두 가지를 기억해 둡시다.
ifwhen은 값을 돌려준다”,
그리고 “범위는 in으로 검사한다“입니다.

이 두 가지는 앞으로도 계속 나옵니다.

다음 장에서는
코드를 묶는 단위인 함수를 다뤄 보겠습니다.